﻿using PickGold.Data.Hsts.General.Persist;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using PickGold.Data.Hsts.General.Extensions;
using PickGold.Data.Hsts.General.Compression;

namespace PickGold.Data.Hsts.Records
{
	public class IndexerPersist<T> : IIndexerPersist<T>
	{
		public Action<BinaryWriter, Func<int, object, T>, int, object> store;
		public Action<BinaryReader, Action<int, object, T>, int, object> load;

		public IIndexerPersist[] Persists { get; private set; }
		public Type Type { get; private set; }
		public readonly Func<Type, MemberInfo, int> MembersOrder;

		public Expression<Action<BinaryWriter, Func<int, object, T>, int, object>> LambdaStore { get; private set; }
		public Expression<Action<BinaryReader, Action<int, object, T>, int, object>> LambdaLoad { get; private set; }

		public IndexerPersist(IIndexerPersist[] persists, Func<Type, MemberInfo, int> membersOrder = null)
		{
			Type = typeof(T);
			MembersOrder = membersOrder;
			Persists = persists;

			LambdaStore = CreateStoreMethod();
			store = LambdaStore.Compile();

			LambdaLoad = CreateLoadMethod();
			load = LambdaLoad.Compile();
		}

		public IndexerPersist(Func<Type, MemberInfo, int> membersOrder = null)
			: this(IndexerPersistHelper.GetDefaultPersists(typeof(T), membersOrder), membersOrder)
		{
		}

		public Expression<Action<BinaryWriter, Func<int, object, T>, int, object>> CreateStoreMethod()
		{
			var writer = Expression.Parameter(typeof(BinaryWriter), "writer");
			var values = Expression.Parameter(typeof(Func<int, object, T>), "func");
			var count = Expression.Parameter(typeof(int), "count");
			var state = Expression.Parameter(typeof(object), "state");

			var idx = Expression.Variable(typeof(int), "idx");
			var callValues = Expression.Call(values, values.Type.GetMethod("Invoke"), idx);

			var body = IndexerPersistHelper.CreateStoreBody(Type, Persists, writer, callValues, idx, count, state, MembersOrder);

			var lambda = Expression.Lambda<Action<BinaryWriter, Func<int, object, T>, int, object>>(body, new ParameterExpression[] { writer, values, count, state, });

			return lambda;
		}

		public Expression<Action<BinaryReader, Action<int, object, T>, int, object>> CreateLoadMethod()
		{
			var reader = Expression.Parameter(typeof(BinaryReader), "reader");
			var values = Expression.Parameter(typeof(Action<int, object, T>), "func");
			var count = Expression.Parameter(typeof(int), "count");
			var state = Expression.Parameter(typeof(object), "state");

			var array = Expression.Variable(typeof(T[]));


			var body = DataType.IsPrimitiveType(Type) ?
					IndexerPersistHelper.SingleSlotCreateLoadBody(Type, false, values, reader, count, state, Persists) :
					Expression.Block(new ParameterExpression[] { array },
					Expression.Assign(array, Expression.New(array.Type.GetConstructor(new Type[] { typeof(int) }), count)),
					array.For(i =>
						{
							return Expression.Block(Expression.Assign(Expression.ArrayAccess(array, i), Expression.New(typeof(T).GetConstructor(new Type[] { }))),
									Expression.Call(values, values.Type.GetMethod("Invoke"), i, Expression.ArrayAccess(array, i)));
						}, Expression.Label(), count),
					IndexerPersistHelper.CreateLoadBody(Type, false, reader, array, count, state, MembersOrder, Persists)
					);

			return Expression.Lambda<Action<BinaryReader, Action<int, object, T>, int, object>>(body, new ParameterExpression[] { reader, values, count, state, });
		}

		public void Store(BinaryWriter writer, Func<int, object, T> values, int count, object state)
		{
			store(writer, values, count, state);
		}

		public void Load(BinaryReader reader, Action<int, object, T> values, int count, object state)
		{
			load(reader, values, count, state);
		}

		#region Examples

		//public class Tick
		//{
		//    public string Symbol { get; set; }
		//    public DateTime Timestamp { get; set; }
		//    public double Bid { get; set; }
		//    public double Ask { get; set; }
		//    public long Volume { get; set; }
		//    public string Provider { get; set; }
		//}

		//public class TickIndexerPersist : IIndexerPersist<Tick>
		//{
		//    public Type Type { get; private set; }
		//    public IIndexerPersist[] Persists { get; private set; }

		//    public readonly Func<Type, MemberInfo, int> MembersOrder;

		//    public TickIndexerPersist(Type type, IIndexerPersist[] persist, Func<Type, MemberInfo, int> membersOrder = null)
		//    {
		//        Persists = persist;
		//        Type = type;
		//        MembersOrder = membersOrder;
		//    }

		//    public void Store(BinaryWriter writer, Func<int, Tick> values, int count)
		//    {
		//        Action[] actions = new Action[6];
		//        MemoryStream[] streams = new MemoryStream[6];

		//        actions[0] = () =>
		//        {
		//            streams[0] = new MemoryStream();
		//            ((StringIndexerPersist)Persists[0]).Store(new BinaryWriter(streams[0]), (idx) => values.Invoke(idx).Symbol, count);
		//        };

		//        actions[1] = () =>
		//        {
		//            streams[1] = new MemoryStream();
		//            ((DateTimeIndexerPersist)Persists[1]).Store(new BinaryWriter(streams[1]), (idx) => values.Invoke(idx).Timestamp, count);
		//        };

		//        actions[2] = () =>
		//        {
		//            streams[2] = new MemoryStream();
		//            ((DoubleIndexerPersist)Persists[2]).Store(new BinaryWriter(streams[2]), (idx) => values.Invoke(idx).Ask, count);
		//        };

		//        actions[3] = () =>
		//        {
		//            streams[3] = new MemoryStream();
		//            ((DoubleIndexerPersist)Persists[3]).Store(new BinaryWriter(streams[3]), (idx) => values.Invoke(idx).Bid, count);
		//        };

		//        actions[4] = () =>
		//        {
		//            streams[4] = new MemoryStream();
		//            ((Int64IndexerPersist)Persists[5]).Store(new BinaryWriter(streams[4]), (idx) => values.Invoke(idx).Volume, count);
		//        };

		//        actions[5] = () =>
		//        {
		//            streams[5] = new MemoryStream();
		//            ((StringIndexerPersist)Persists[5]).Store(new BinaryWriter(streams[5]), (idx) => values.Invoke(idx).Provider, count);
		//        };

		//        Parallel.Invoke(actions);

		//        for (int i = 0; i < actions.Length; i++)
		//        {
		//            var stream = streams[i];
		//            using (stream)
		//            {
		//                CountCompression.Serialize(writer, (ulong)stream.Length);
		//                writer.Write(stream.GetBuffer(), 0, (int)stream.Length);
		//            }
		//        }
		//    }

		//    public void Load(BinaryReader reader, Action<int, Tick> values, int count)
		//    {
		//        Tick[] array = new Tick[count];
		//        for (int i = 0; i < count; i++)
		//        {
		//            var item = new Tick();
		//            array[i] = item;
		//            values(i, item);
		//        }

		//        Action[] actions = new Action[6];
		//        byte[][] buffers = new byte[6][];

		//        for (int i = 0; i < 6; i++)
		//            buffers[i] = reader.ReadBytes((int)CountCompression.Deserialize(reader));

		//        actions[0] = () =>
		//        {
		//            using (MemoryStream ms = new MemoryStream(buffers[0]))
		//                ((IIndexerPersist<String>)Persists[0]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Symbol = value; }, count);
		//        };

		//        actions[1] = () =>
		//        {
		//            using (MemoryStream ms = new MemoryStream(buffers[1]))
		//                ((IIndexerPersist<DateTime>)Persists[1]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Timestamp = value; }, count);
		//        };

		//        actions[2] = () =>
		//        {
		//            using (MemoryStream ms = new MemoryStream(buffers[2]))
		//                ((IIndexerPersist<Double>)Persists[2]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Bid = value; }, count);
		//        };

		//        actions[3] = () =>
		//        {
		//            using (MemoryStream ms = new MemoryStream(buffers[3]))
		//                ((IIndexerPersist<Double>)Persists[3]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Ask = value; }, count);
		//        };

		//        actions[4] = () =>
		//        {
		//            using (MemoryStream ms = new MemoryStream(buffers[4]))
		//                ((IIndexerPersist<Int64>)Persists[4]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Volume = value; }, count);
		//        };

		//        actions[5] = () =>
		//        {
		//            using (MemoryStream ms = new MemoryStream(buffers[5]))
		//                ((IIndexerPersist<String>)Persists[5]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Provider = value; }, count);
		//        };

		//        Parallel.Invoke(actions);
		//    }
		//}

		#endregion
	}

	internal static class IndexerPersistHelper
	{
		internal static Expression CreateLoadBody(Type type, bool isData, Expression reader, ParameterExpression array, ParameterExpression count, ParameterExpression state, Func<Type, MemberInfo, int> membersOrder, IIndexerPersist[] persists)
		{
			var countOfType = type.GetPublicReadWritePropertiesAndFields().Count();

			var list = new List<Expression>();

			var actionsArray = Expression.Variable(typeof(Action[]));
			list.Add(Expression.Assign(actionsArray, Expression.New(actionsArray.Type.GetConstructor(new Type[] { typeof(int) }), Expression.Constant(countOfType))));

			var buffers = Expression.Variable(typeof(byte[][]));
			list.Add(Expression.Assign(buffers, Expression.New(buffers.Type.GetConstructor(new Type[] { typeof(int) }), Expression.Constant(countOfType))));

			list.Add(buffers.For(i =>
				Expression.Assign(Expression.ArrayAccess(buffers, i),
					Expression.Call(reader, typeof(BinaryReader).GetMethod("ReadBytes"),
						Expression.Convert(Expression.Call(typeof(CountCompression).GetMethod("Deserialize"), reader), typeof(int)))), Expression.Label(), Expression.Constant(countOfType))
					);

			var ms = Expression.Variable(typeof(MemoryStream));

			var j = 0;
			foreach (var member in type.GetPublicReadWritePropertiesAndFields())
				list.Add(Expression.Assign(Expression.ArrayAccess(actionsArray, Expression.Constant(j)),
										   Expression.Lambda(ms.Using(Expression.New(typeof(MemoryStream).GetConstructor(new Type[] { typeof(byte[]) }),
											Expression.ArrayAccess(buffers, Expression.Constant(j))), GetLoadPersistCall(j++, isData, member, array, ms, count, state, persists))))
						);

			list.Add(Expression.Call(typeof(Parallel).GetMethod("Invoke", new Type[] { typeof(Action[]) }), actionsArray));

			return Expression.Block(new ParameterExpression[] { actionsArray, buffers, ms }, list);
		}

		private static Expression GetLoadPersistCall(int index, bool isData, MemberInfo member, ParameterExpression array, ParameterExpression ms, ParameterExpression count, ParameterExpression state, IIndexerPersist[] persists)
		{
			//  ((IIndexerPersist<string>)persist[0]).Load(new BinaryReader(ms), (idx, value) => { array[idx].Slot0 = value; }, count);

			var idx = Expression.Variable(typeof(int), "idx");
			var value = Expression.Variable(member.GetPropertyOrFieldType(), "value");

			var field = isData ? Expression.PropertyOrField(Expression.ArrayAccess(array, idx).Value(), member.Name) : Expression.PropertyOrField(Expression.ArrayAccess(array, idx), member.Name);
			var pt = Expression.Convert(Expression.Constant(persists[index]), persists[index].GetType());
			var lm = persists[index].GetType().GetMethod("Load");
			var nbr = Expression.New(typeof(BinaryReader).GetConstructor(new Type[] { typeof(MemoryStream) }), ms);
			var gt = typeof(Action<,,>).MakeGenericType(new Type[] { typeof(int), typeof(object), member.GetPropertyOrFieldType(), });
			var fa = Expression.Assign(field, value);
			var lambda = Expression.Lambda(gt, fa, idx, state, value);
			var call = Expression.Call(pt, lm, nbr, lambda, count, state);
			return call;
		}

		internal static Expression SingleSlotCreateLoadBody(Type type, bool isData, Expression values, Expression reader, ParameterExpression count, ParameterExpression state, IIndexerPersist[] persists)
		{
			var list = new List<Expression>();
			var buffers = Expression.Variable(typeof(byte[]));
			list.Add(Expression.Assign(buffers, Expression.Call(reader, typeof(BinaryReader).GetMethod("ReadBytes"), Expression.Convert(Expression.Call(typeof(CountCompression).GetMethod("Deserialize"), reader), typeof(int)))));

			var buffer = Expression.New(typeof(MemoryStream).GetConstructor(new Type[] { typeof(byte[]) }), buffers);
			var pt = Expression.Convert(Expression.Constant(persists[0]), persists[0].GetType());
			var ms = Expression.Variable(typeof(MemoryStream));
			var nms = Expression.New(typeof(BinaryReader).GetConstructor(new Type[] { typeof(MemoryStream) }), ms);
			var idx = Expression.Variable(typeof(int), "idx");
			var lm = persists[0].GetType().GetMethod("Load");
			var value = Expression.Variable(type, "value");
			var cm = values.Type.GetMethod("Invoke");
			var vc = isData
						   ? Expression.Call(values, cm, idx, state, Expression.New(typeof(Data<>).MakeGenericType(type).GetConstructor(new Type[] { type }), value))
						   : Expression.Call(values, cm, idx, state, value);
			var lambda = Expression.Lambda(vc, idx, state, value);
			var call = Expression.Call(pt, lm, nms, lambda, count, state);
			var ue = ms.Using(buffer, call);
			list.Add(ue);

			return Expression.Block(new ParameterExpression[] { buffers }, list);
		}

		internal static Expression CreateStoreBody(Type type, IIndexerPersist[] persists, Expression writer, Expression callValues, ParameterExpression idx, ParameterExpression count, ParameterExpression state, Func<Type, MemberInfo, int> membersOrder)
		{
			var list = new List<Expression>();
			var itemsCount = DataTypeUtils.GetPublicMembers(type, membersOrder).Count();

			//Create body for single item.
			if (itemsCount <= 1)
			{
				var persist = Expression.Convert(Expression.Constant(persists[0]), persists[0].GetType());
				var ms = Expression.Variable(typeof(MemoryStream), "ms");
				var func = Expression.Lambda(itemsCount == 0 ? callValues : Expression.PropertyOrField(callValues, DataTypeUtils.GetPublicMembers(type, membersOrder).First().Name), idx, state);

				return ms.Using(Expression.Block(
						Expression.Assign(ms, Expression.New(typeof(MemoryStream).GetConstructor(new Type[] { }))),
						Expression.Call(persist, persist.Type.GetMethod("Store"), Expression.New(typeof(BinaryWriter).GetConstructor(new Type[] { typeof(MemoryStream) }), ms), func, count, state),
						Expression.Call(typeof(CountCompression).GetMethod("Serialize"), writer, Expression.ConvertChecked(Expression.Property(ms, "Length"), typeof(ulong))),
						Expression.Call(writer, typeof(BinaryWriter).GetMethod("Write", new Type[] { typeof(byte[]), typeof(int), typeof(int) }),
							Expression.Call(ms, typeof(MemoryStream).GetMethod("GetBuffer")), Expression.Constant(0), Expression.Convert(Expression.Property(ms, "Length"), typeof(int)))
					));
			}
			else
			{
				var streams = Expression.Variable(typeof(MemoryStream[]), "streams");
				var actions = Expression.Variable(typeof(Action[]), "actions");

				list.Add(Expression.Assign(streams, Expression.New(typeof(MemoryStream[]).GetConstructor(new Type[] { typeof(int) }), Expression.Constant(itemsCount, typeof(int)))));
				list.Add(Expression.Assign(actions, Expression.New(typeof(Action[]).GetConstructor(new Type[] { typeof(int) }), Expression.Constant(itemsCount, typeof(int)))));

				var counter = 0;
				foreach (var member in DataTypeUtils.GetPublicMembers(type, membersOrder))
				{
					var persist = Expression.Convert(Expression.Constant(persists[counter]), persists[counter].GetType());
					var ms = Expression.ArrayAccess(streams, Expression.Constant(counter, typeof(int)));
					var func = Expression.Lambda(Expression.PropertyOrField(callValues, member.Name), idx, state);

					var writerNew = Expression.New(typeof(BinaryWriter).GetConstructor(new Type[] { typeof(MemoryStream) }), ms);

					var action = Expression.Lambda(Expression.Block(
						   Expression.Assign(ms, Expression.New(typeof(MemoryStream).GetConstructor(new Type[] { }))),
						   Expression.Call(persist, persist.Type.GetMethod("Store"), writerNew, func, count, state)
						));

					list.Add(Expression.Assign(Expression.ArrayAccess(actions, Expression.Constant(counter)), action));
					counter++;
				}

				list.Add(Expression.Call(typeof(Parallel).GetMethod("Invoke", new Type[] { typeof(Action[]) }), actions));

				list.Add(streams.For(
					(i) =>
					{
						var stream = Expression.Variable(typeof(MemoryStream), "stream");

						return stream.Using(Expression.Block(
							   Expression.Assign(stream, Expression.ArrayAccess(streams, i)),
							   Expression.Call(typeof(CountCompression).GetMethod("Serialize"), writer, Expression.ConvertChecked(Expression.Property(stream, "Length"), typeof(ulong))),
							   Expression.Call(writer, typeof(BinaryWriter).GetMethod("Write", new Type[] { typeof(byte[]), typeof(int), typeof(int) }),
								   Expression.Call(stream, typeof(MemoryStream).GetMethod("GetBuffer")), Expression.Constant(0), Expression.Convert(Expression.Property(stream, "Length"), typeof(int)))
							));
					},
					 Expression.Label()
				));

				return Expression.Block(new ParameterExpression[] { actions, streams }, list);
			}
		}

		public static IIndexerPersist[] GetDefaultPersists(Type type, Func<Type, MemberInfo, int> membersOrder = null)
		{
			if (DataType.IsPrimitiveType(type))
				return new IIndexerPersist[] { GetDefaultPersist(type) };

			var list = new List<IIndexerPersist>();
			foreach (var member in type.GetPublicReadWritePropertiesAndFields())
				list.Add(GetDefaultPersist(member.GetPropertyOrFieldType()));

			return list.ToArray();
		}

		private static IIndexerPersist GetDefaultPersist(Type type)
		{
			if (type == null)
				throw new ArgumentNullException();

			if (type == typeof(bool))
				return new BooleanIndexerPersist();

			if (type == typeof(char))
				return new CharIndexerPersist();

			if (type == typeof(byte))
				return new ByteIndexerPersist();

			if (type == typeof(sbyte))
				return new SByteIndexerPersist();

			if (type == typeof(short))
				return new Int16IndexerPersist();

			if (type == typeof(ushort))
				return new UInt16IndexerPersist();

			if (type == typeof(int))
				return new Int32IndexerPersist();

			if (type == typeof(uint))
				return new UInt32IndexerPersist();

			if (type == typeof(long))
				return new Int64IndexerPersist();

			if (type == typeof(ulong))
				return new UInt64IndexerPersist();

			if (type == typeof(float))
				return new SingleIndexerPersist();

			if (type == typeof(double))
				return new DoubleIndexerPersist();

			if (type == typeof(decimal))
				return new DecimalIndexerPersist();

			if (type == typeof(string))
				return new StringIndexerPersist();

			if (type == typeof(DateTime))
				return new DateTimeIndexerPersist();

			if (type == typeof(TimeSpan))
				return new TimeSpanIndexerPersist();

			if (type == typeof(byte[]))
				return new ByteArrayIndexerPersist();

			throw new NotSupportedException(type.ToString());
		}
	}
}
